"""
Integration tests for TTS API endpoints.

Tests the full request/response cycle with real components,
no mocking except for external API calls.
"""

import json
import base64
import tempfile
from pathlib import Path

import pytest
from fastapi import status
from unittest.mock import patch, AsyncMock, MagicMock

from tldw_Server_API.app.api.v1.endpoints import audio as audio_endpoints

pytestmark = [pytest.mark.integration, pytest.mark.asyncio]

# ========================================================================
# TTS Generate Endpoint Tests
# ========================================================================

class TestTTSGenerateEndpoint:
    """Tests for the /api/v1/audio/speech endpoint."""
    @patch('tldw_Server_API.app.core.TTS.adapters.openai_adapter.httpx.AsyncClient.post')
    async def test_generate_basic_audio(self, mock_post, test_client, auth_headers):
        """Test basic TTS generation endpoint."""
        # Mock OpenAI API response
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b"fake_audio_data"
        mock_response.headers = {"content-type": "audio/mpeg"}
        mock_post.return_value = mock_response

        response = test_client.post(
            "/api/v1/audio/speech",
            json={
                "input": "Hello, this is a test.",
                "voice": "alloy",
                "model": "tts-1",
                "response_format": "mp3",
                "stream": False
            },
            headers=auth_headers
        )

        assert response.status_code == status.HTTP_200_OK
        assert response.headers.get("content-type") == "audio/mpeg"
        assert len(response.content) > 0

    @pytest.mark.streaming
    @patch('tldw_Server_API.app.core.TTS.adapters.openai_adapter.httpx.AsyncClient.post')
    async def test_generate_basic_audio_streaming(self, mock_post, test_client, auth_headers):
        """Test basic TTS generation endpoint in streaming mode (OpenAI)."""

        async def mock_iter_bytes(chunk_size=1024):
            for chunk in [b"chunk1", b"chunk2"]:
                yield chunk

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.aiter_bytes = mock_iter_bytes
        mock_response.raise_for_status = MagicMock()
        mock_post.return_value = mock_response

        response = test_client.post(
            "/api/v1/audio/speech",
            json={
                "input": "Hello, this is a streaming test.",
                "voice": "alloy",
                "model": "tts-1",
                "response_format": "mp3",
                "stream": True,
            },
            headers=auth_headers,
        )

        assert response.status_code == status.HTTP_200_OK
        assert response.headers.get("content-type") == "audio/mpeg"
        chunks = list(response.iter_bytes())
        assert len(chunks) > 0

    async def test_generate_without_provider(self, test_client, auth_headers):
        """Test generation using default provider."""
        async def mock_stream(*args, **kwargs):
            yield b"audio_data"

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test without provider",
                    "voice": "nova",
                    "response_format": "mp3",
                    "stream": False
                    # No provider specified, should use default
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK

    async def test_generate_with_voice_settings(self, test_client, auth_headers):
        """Test generation with voice settings."""
        async def mock_stream(*args, **kwargs):
            yield b"custom_audio"

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Custom voice test",
                    "voice": "rachel",
                    "response_format": "mp3",
                    "stream": False,
                    "extra_params": {
                        "stability": 0.5,
                        "similarity_boost": 0.75
                    }
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK

            # Verify extra params were passed
            mock_generate_speech.assert_called_once()
            call_args = mock_generate_speech.call_args[0][0]
            assert getattr(call_args, 'extra_params', None) is not None

    async def test_generate_with_invalid_provider(self, test_client, auth_headers):
        """Test generation with invalid provider."""
        async def mock_stream(*args, **kwargs):
            # Simulate service emitting an error payload instead of raising
            yield b"ERROR: No adapter"

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_gen:
            mock_gen.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test",
                    "voice": "alloy",
                    "model": "unknown-model-xyz",
                    "response_format": "mp3",
                    "stream": False
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK

    async def test_generate_with_long_text(self, test_client, auth_headers):
        """Test generation with long text that needs chunking."""
        long_text = " ".join(["This is sentence number {}.".format(i) for i in range(500)])

        async def mock_stream(*args, **kwargs):
            yield b"long_audio"

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": long_text,
                    "voice": "alloy",
                    "response_format": "mp3",
                    "stream": False
                },
                headers=auth_headers
            )

            # Should handle long text appropriately
            assert response.status_code in [status.HTTP_200_OK, status.HTTP_413_CONTENT_TOO_LARGE]

# ========================================================================
# TTS Streaming Endpoint Tests
# ========================================================================

class TestTTSStreamingEndpoint:
    """Tests for streaming via /api/v1/audio/speech with stream=true."""

    @pytest.mark.streaming
    async def test_streaming_generation(self, test_client, auth_headers):
        """Test streaming TTS generation."""

        async def mock_stream():
            for chunk in [b"chunk1", b"chunk2", b"chunk3"]:
                yield chunk

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_stream_gen:
            mock_stream_gen.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Stream this text",
                    "voice": "echo",
                    "response_format": "mp3",
                    "stream": True
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK

            # Collect streamed chunks
            chunks = []
            for chunk in response.iter_bytes():
                chunks.append(chunk)

            assert len(chunks) > 0

    @pytest.mark.streaming
    async def test_streaming_with_error(self, test_client, auth_headers):
        """Test streaming handles errors gracefully."""

        async def mock_error_stream():
            yield b"chunk1"
            raise Exception("Stream error")

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_stream:
            mock_stream.return_value = mock_error_stream()

            try:
                response = test_client.post(
                    "/api/v1/audio/speech",
                    json={
                        "input": "Error test",
                        "voice": "alloy",
                        "response_format": "mp3",
                        "stream": True
                    },
                    headers=auth_headers
                )
                # Either we get a 200 with initial chunks, or a 500 error response
                assert response.status_code in [status.HTTP_200_OK, status.HTTP_500_INTERNAL_SERVER_ERROR]
                if response.status_code == status.HTTP_200_OK:
                    chunks = list(response.iter_bytes())
                    assert len(chunks) > 0
            except Exception:
                # Some Starlette versions propagate generator errors; accept as handled for test purposes
                assert True

    @pytest.mark.streaming
    async def test_streaming_quota_exceeded_maps_to_402(self, test_client, auth_headers):
        """Streaming quota exceeded should ideally map to HTTP 402."""
        from tldw_Server_API.app.core.TTS.tts_exceptions import TTSQuotaExceededError

        audio_endpoints.limiter._storage.reset()

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = TTSQuotaExceededError("Character quota exceeded")

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test",
                    "voice": "rachel",
                    "response_format": "mp3",
                    "stream": True
                },
                headers=auth_headers
            )

            # Depending on streaming mechanics, frameworks may return 402 or 500
            # when the generator raises immediately. Accept both, preferring 402.
            assert response.status_code in [status.HTTP_402_PAYMENT_REQUIRED, status.HTTP_500_INTERNAL_SERVER_ERROR]

# ========================================================================
# Provider Management Endpoint Tests
# ========================================================================

class TestProviderManagementEndpoints:
    """Tests for TTS provider management endpoints under /api/v1/audio."""

    async def test_list_providers(self, test_client, auth_headers):
        """Test listing available TTS providers."""
        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.get_capabilities') as mock_caps, \
             patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.list_voices') as mock_voices:
            mock_caps.return_value = {
                "openai": {"models": ["tts-1", "tts-1-hd"]},
                "elevenlabs": {"models": ["eleven_multilingual_v2"]},
                "kokoro": {"models": ["kokoro"]},
            }
            mock_voices.return_value = {
                "openai": [{"id": "alloy"}],
                "elevenlabs": [{"id": "rachel"}],
            }

            response = test_client.get(
                "/api/v1/audio/providers",
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK
            data = response.json()
            assert "providers" in data and "voices" in data
            assert "openai" in data["providers"]
            assert "elevenlabs" in data["providers"]

    async def test_get_provider_info(self, test_client, auth_headers):
        """Test getting specific provider information."""
        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.get_capabilities') as mock_caps:
            mock_caps.return_value = {
                "openai": {"models": ["tts-1", "tts-1-hd"], "voices": ["alloy", "echo"]}
            }

            response = test_client.get(
                "/api/v1/audio/providers",
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK
            data = response.json()
            assert "openai" in data["providers"]
            assert "tts-1" in data["providers"]["openai"].get("models", [])

    async def test_switch_default_provider(self, test_client, auth_headers):
        """Test switching the default TTS provider."""
        response = test_client.post(
            "/api/v1/audio/providers/default",
            json={"provider": "elevenlabs"},
            headers=auth_headers
        )

        if response.status_code == status.HTTP_200_OK:
            data = response.json()
            assert data["message"] == "Default provider updated"
            assert data["provider"] == "elevenlabs"

# ========================================================================
# Voice Management Endpoint Tests
# ========================================================================

class TestVoiceManagementEndpoints:
    """Tests for voice management endpoints under /api/v1/audio."""

    async def test_list_voices(self, test_client, auth_headers):
        """Test listing available voices for a provider."""
        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.list_voices') as mock_voices:
            mock_voices.return_value = {
                "openai": [
                    {"id": "alloy", "name": "Alloy", "gender": "neutral"},
                    {"id": "echo", "name": "Echo", "gender": "male"},
                    {"id": "nova", "name": "Nova", "gender": "female"}
                ]
            }

            response = test_client.get(
                "/api/v1/audio/voices/catalog?provider=openai",
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK
            data = response.json()
            assert isinstance(data, dict)
            assert "openai" in data
            assert isinstance(data["openai"], list)
            assert len(data["openai"]) == 3
            assert any(v["id"] == "alloy" for v in data["openai"])

    async def test_get_voice_details(self, test_client, auth_headers):
        """Test getting details for a specific voice."""
        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.list_voices') as mock_voices:
            mock_voices.return_value = {
                "elevenlabs": [
                    {"id": "rachel", "name": "Rachel", "gender": "female"}
                ]
            }

            response = test_client.get(
                "/api/v1/audio/voices/catalog?provider=elevenlabs",
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK
            data = response.json()
            assert "elevenlabs" in data
            assert data["elevenlabs"][0]["id"] == "rachel"

# ========================================================================
# File Download Endpoint Tests
# ========================================================================

class TestFileDownloadEndpoints:
    """Test audio file download endpoints."""

    async def test_download_generated_audio(self, test_client, auth_headers, sample_audio_bytes):
        """Test downloading generated audio as file."""
        async def mock_stream(*args, **kwargs):
            yield sample_audio_bytes

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = lambda *args, **kwargs: mock_stream()

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Download this audio",
                    "voice": "alloy",
                    "response_format": "wav",
                    "stream": False
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_200_OK
            assert response.headers["content-type"] == "audio/wav"
            assert "content-disposition" in response.headers
            assert len(response.content) > 0

# ========================================================================
# Error Handling Tests
# ========================================================================

class TestErrorHandling:
    """Test error handling in TTS endpoints."""

    async def test_rate_limit_error_handling(self, test_client, auth_headers):
        """Test handling of rate limit errors."""
        from tldw_Server_API.app.core.TTS.tts_exceptions import rate_limit_error

        audio_endpoints.limiter._storage.reset()

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = rate_limit_error("OpenAITTS", retry_after=60)

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test",
                    "voice": "alloy",
                    "response_format": "mp3",
                    "stream": False
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
            assert "detail" in response.json()

    async def test_quota_exceeded_error(self, test_client, auth_headers):
        """Test handling of quota exceeded errors."""
        from tldw_Server_API.app.core.TTS.tts_exceptions import TTSQuotaExceededError

        audio_endpoints.limiter._storage.reset()

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = TTSQuotaExceededError("Character quota exceeded")

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test",
                    "voice": "rachel",
                    "response_format": "mp3",
                    "stream": False
                },
                headers=auth_headers
            )

            assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED
            data = response.json()
            assert "quota" in str(data).lower()

    async def test_provider_not_configured(self, test_client, auth_headers):
        """Test handling of unconfigured provider errors."""
        from tldw_Server_API.app.core.TTS.tts_exceptions import TTSProviderNotConfiguredError

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = TTSProviderNotConfiguredError("Provider not configured")

            response = test_client.post(
                "/api/v1/audio/speech",
                json={
                    "input": "Test",
                    "voice": "voice1",
                    "response_format": "mp3",
                    "stream": False
                },
                headers=auth_headers
            )

            assert response.status_code in [status.HTTP_503_SERVICE_UNAVAILABLE, status.HTTP_429_TOO_MANY_REQUESTS]

# ========================================================================
# Batch Processing Tests
# ========================================================================

class TestBatchProcessing:
    """Simulate batch TTS by multiple calls to /api/v1/audio/speech."""

    async def test_batch_tts_generation(self, test_client, auth_headers):
        """Test batch generation by issuing multiple requests."""
        async def mock_stream(*args, **kwargs):
            yield b"audio"

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = lambda *args, **kwargs: mock_stream()

            payloads = [
                {"input": "First text", "voice": "alloy", "response_format": "mp3", "stream": False},
                {"input": "Second text", "voice": "echo", "response_format": "mp3", "stream": False},
                {"input": "Third text", "voice": "nova", "response_format": "mp3", "stream": False},
            ]
            responses = [
                test_client.post("/api/v1/audio/speech", json=p, headers=auth_headers)
                for p in payloads
            ]
            assert all(r.status_code in [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS] for r in responses)

    async def test_batch_with_partial_failures(self, test_client, auth_headers):
        """Test batch processing with some failures."""
        from tldw_Server_API.app.core.TTS.tts_exceptions import TTSGenerationError

        call = {"n": 0}

        def side_effect(*args, **kwargs):
            async def _gen():
                call["n"] += 1
                if call["n"] == 2:
                    raise TTSGenerationError("Failed")
                yield b"audio"
            return _gen()

        with patch('tldw_Server_API.app.core.TTS.tts_service_v2.TTSServiceV2.generate_speech') as mock_generate_speech:
            mock_generate_speech.side_effect = side_effect

            payloads = [
                {"input": "Success 1", "voice": "alloy", "response_format": "mp3", "stream": False},
                {"input": "Failure", "voice": "echo", "response_format": "mp3", "stream": False},
                {"input": "Success 2", "voice": "nova", "response_format": "mp3", "stream": False},
            ]
            responses = [
                test_client.post("/api/v1/audio/speech", json=p, headers=auth_headers)
                for p in payloads
            ]
            assert responses[0].status_code in [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]
            assert responses[1].status_code in [status.HTTP_500_INTERNAL_SERVER_ERROR, status.HTTP_429_TOO_MANY_REQUESTS]
            assert responses[2].status_code in [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]
